package org.example.netty.test.nio.java;

import lombok.extern.slf4j.Slf4j;
import org.example.netty.test.nio.bean.Client;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @author zp
 * @version 1.0
 * @description: 服务端的测试代码
 * @date 2023/2/21 18:54
 */
@Slf4j
public class ServerSocketChannelDemo {
    private Charset charset = StandardCharsets.UTF_8;

    public static void main(String[] args) throws IOException {
        ServerSocketChannelDemo socketChannelDemo = new ServerSocketChannelDemo();
        socketChannelDemo.startServer();
    }

    public void startServer() throws IOException {
        // 1、获取Selector选择器
        Selector selector = Selector.open();
        // 2、获取通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverChannel.socket();
        // 3.设置为非阻塞
        serverChannel.configureBlocking(false);
        // 4、绑定连接
        InetSocketAddress address = new InetSocketAddress(18899);
        serverSocket.bind(address);
        // 5、将通道注册到选择器上,并注册的IO事件为：“接收新连接”
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("serverChannel is linstening…");
        // 6、轮询感兴趣的I/O就绪事件（选择键集合）
        while (selector.select() > 0) {
            // 7、获取选择键集合
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                // 8、获取单个的选择键，并处理
                SelectionKey key = it.next();
                // 9、判断key是具体的什么事件，是否为新连接事件
                if (key.isAcceptable()) {
                    // 10、若接受的事件是“新连接”事件,就获取客户端新连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    if (socketChannel == null) {
                        continue;
                    }
                    // 11、客户端新连接，切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 12、将客户端新连接通道注册到selector选择器上
                    SelectionKey socketKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    // 余下为业务处理
                    Client client = new Client();
                    client.remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
                    socketKey.attach(client);
                    log.debug(socketChannel.getRemoteAddress() + "连接成功…");
                } else if (key.isReadable()) {
                    processData(key);
                }
                // NIO的特点只会累加，已选择的键的集合不会删除
                // 如果不删除，下一次又会被select函数选中
                it.remove();
            }
        }
    }

    /**
     * @param key
     * @description: 处理客户端传输过来的数据
     * @author zp
     * @date: 2023/2/21 19:22
     */
    private void processData(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        Client client = (Client) key.attachment();
        ByteBuffer buffer = client.buffer;
        int num = 0;
        buffer.clear();
        while ((num = socketChannel.read(buffer)) > 0) {
            buffer.flip();
            //客户端发送过来的，首先处理文件名
            if (null == client.fileName) {
                if (buffer.capacity() < 4) {
                    continue;
                }
                int fileNameLen = buffer.getInt();
                byte[] fileNameBytes = new byte[fileNameLen];
                client.fileLength = fileNameLen;
                buffer.get(fileNameBytes);
                // 文件名
                String fileName = new String(fileNameBytes, charset);
                File directory = new File(IOUtil.builderResourcePath("file"));
                if (!directory.exists()) {
                    directory.mkdirs();
                }
                log.info("NIO 传输目标dir：{}", directory);
                client.fileName = fileName;
                String fullName = directory.getAbsolutePath() + File.separatorChar + fileName;
                log.info("NIO 传输目标文件：{}", fullName);
                File file = new File(fullName.trim());
                if (!file.exists()) {
                    file.createNewFile();
                }
                FileChannel fileChannel = new FileOutputStream(file).getChannel();
                client.outChannel = fileChannel;
                if (buffer.capacity() < 8) {
                    continue;
                }
                // 文件长度
                client.fileLength = buffer.getLong();
                client.startTime = System.currentTimeMillis();
                log.debug("NIO 传输开始：");
                client.receiveLength += buffer.capacity();
                if (buffer.capacity() > 0) {
                    // 写入文件
                    client.outChannel.write(buffer);
                }
                if (client.isFinished()) {
                    finished(key, client);
                }
                buffer.clear();
            }
            //客户端发送过来的，最后是文件内容
            else {
                client.receiveLength += buffer.capacity();
                // 写入文件
                client.outChannel.write(buffer);
                if (client.isFinished()) {
                    finished(key, client);
                }
                buffer.clear();
            }
        }
        key.cancel();
        // 调用close为-1 到达末尾
        if (num == -1) {
            finished(key, client);
            buffer.clear();
        }
    }

    private void finished(SelectionKey key, Client client) {
        IOUtil.closeQuietly(client.outChannel);
        log.info("上传完毕");
        key.cancel();
        log.debug("文件接收成功,File Name：" + client.fileName);
        log.debug(" Size：" + IOUtil.getFormatFileSize(client.fileLength) + " KB");
        long endTime = System.currentTimeMillis();
        log.debug("NIO IO 传输毫秒数：" + (endTime - client.startTime));
    }
}
